package com.bxait.toymail.search.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.bxait.com.to.es.SkuEsModel;
import com.bxait.com.utils.R;
import com.bxait.com.vo.product.BrandVo;
import com.bxait.com.vo.product.CateLogVo;
import com.bxait.toymail.search.config.ToyMailElasticSearchConfig;
import com.bxait.toymail.search.constant.EsConstant;
import com.bxait.toymail.search.feign.ProductFeignService;
import com.bxait.toymail.search.service.MallSearchService;
import com.bxait.toymail.search.vo.AttrResponseVo;
import com.bxait.toymail.search.vo.AttrVo;
import com.bxait.toymail.search.vo.SearchParam;
import com.bxait.toymail.search.vo.SearchResult;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.*;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/*
@description:
@ClassName MallSearchServiceImpl
@author Chen
@create 2023-04-15 19:06
@Version 1.0
*/
@Slf4j
@Service("mallSearchService")
public class MallSearchServiceImpl implements MallSearchService {

    @Resource
    private RestHighLevelClient client;

    @Resource
    ProductFeignService productFeignService;

    /*
     * @param param
     * @==============History===============<br/>
     * @Description //   在es检索数据
     * @Date 2023/4/15 2023/4/15
     * @Author < a href="2277411582@qq.com">chen</ a>
     * @return SearchResult
     */
    @Override
    public SearchResult search(SearchParam param) {
        SearchResult result = null;
        // 1.构造es查询条件
        SearchRequest searchRequest = buildSearchRequest(param);
        // 2.执行查询,解析es返回数据格式
        try {
            //2、执行检索请求
            SearchResponse response = client.search(searchRequest, ToyMailElasticSearchConfig.COMMON_OPTIONS);

            //3、分析响应数据封装成我们需要的格式
            result = buildSearchResult(response, param);
        } catch (IOException e) {
            e.printStackTrace();
        }
//         3.返回数据
//        SearchResult result = buildResult(param);
        return result;
    }

    private SearchResult buildResult(SearchParam param) {
        SearchResult result = new SearchResult();
        // 获取sku数据
        R r = productFeignService.getSkuInfo();
        if(r.getCode() == 0){
            result = r.getData(new TypeReference<SearchResult>() {});
        }
        return result;
    }


    /*
     * @param param 查询参数
     * @==============History===============<br/>
     * @Description //   根据参数构造es查询条件
     * @Date 2023/4/15 2023/4/15
     * @Author < a href="2277411582@qq.com">chen</ a>
     * @return
     */
    private SearchRequest buildSearchRequest(SearchParam param) {
        // 构建dsl语句的SearchSourceBuilder
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        // 1.构建bool查询条件
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        // 构建关键字检索 使用must-模糊匹配商品标题信息  bool->must->match
        String keyword = param.getKeyword();
        if(!StringUtils.isEmpty(keyword)){
            MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery(EsConstant.SKU_TITLE, keyword);
            boolQueryBuilder.must(matchQueryBuilder);
        }

        // 构建分类请求 使用filter-过滤,不参与评分 bool->filter->catalogId->term
        Long catalog3Id = param.getCatalog3Id();
        if(catalog3Id != null){
            QueryBuilder queryBuilder = QueryBuilders.termQuery(EsConstant.CATALOG_ID,catalog3Id);
            boolQueryBuilder.filter(queryBuilder);
        }

        // 构建品牌请求使用 filter-过滤,不参与评分 bool->filter->brandId->terms
        List<Long> brandId = param.getBrandId();
        if(brandId != null && brandId.size() > 0){
            TermsQueryBuilder queryBuilder = QueryBuilders.termsQuery(EsConstant.BRAND_ID, brandId);
            boolQueryBuilder.filter(queryBuilder);
        }

        // 构建属性查询,属性是按照内部nested的方式在es中保存,es会将其处理为一个扁平化的json,所有需要使用nested来进行构建查询SDSL
        List<String> attrs = param.getAttrs();
        // 有多少个属性就放构建多少个NestedQueryBuilder
        if(attrs != null && attrs.size() > 0){
            for (String attr : attrs) {
                // attr保存的格式是1_1.5存:2.5存  _前是属性的id,_后是属性带的值
                BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
                String[] str = attr.split("_");
                // 检索的商品的id
                String attrId = str[0];
                // 检索的商品属性值  以:号进行分割
                String attrIdValue = str[1];
                // 当前这个属性的检索值
                String[] attrIdValueS = attrIdValue.split(":");

                queryBuilder.must(QueryBuilders.termQuery(EsConstant.NESTED_ATTR_ID,attrId));
                queryBuilder.must(QueryBuilders.termsQuery(EsConstant.NESTED_ATTR_VALUES,attrIdValueS));
                // String path(就传我们上架的时候放的那个路径), QueryBuilder query, ScoreMode scoreMode
                NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery(EsConstant.NESTED_ATTR_PATH,queryBuilder,ScoreMode.None);
                boolQueryBuilder.filter(nestedQueryBuilder);
            }
        }


        // 构建是否有库存 0(无货) 1(有货)
        Integer hasStock = param.getHasStock();
        if(hasStock != null){
            // String name, String value
            TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery(EsConstant.HAS_STOCK,hasStock == 1);
            boolQueryBuilder.filter(termQueryBuilder);
        }

        // 构建价格区间查询
        String skuPrice = param.getSkuPrice();
        if(!StringUtils.isEmpty(skuPrice)){
            // 价格区间 1_500/_500/500_
            RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery(EsConstant.SKU_PRICE);
            String[] str = skuPrice.split("_");
            if(str.length == 2){
                // 那就是1_500/_500 这两种情况
                // 如果价格区间是由_开头的
                if(skuPrice.startsWith("_")){
                    rangeQueryBuilder.lte(str[1]);
                }else {
                    rangeQueryBuilder.gte(str[0]).lte(str[1]);
                }
            }
            if(str.length == 1){
                // 那就是1_ 这一种情况
                rangeQueryBuilder.gte(str[0]);
            }
            boolQueryBuilder.filter(rangeQueryBuilder);
        }
        // 将构造好的查询条件封装
        searchSourceBuilder.query(boolQueryBuilder);

        // 2.构建排序
        String sort = param.getSort();
        if(!StringUtils.isEmpty(sort)){
            // sort=saleCount_asc/desc
            String[] str = sort.split("_");
            // 获取排序列
            String orderField = str[0];
            // 获取排序方式
            SortOrder orders = str[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
            searchSourceBuilder.sort(orderField, orders);
        }


        // 3.构建分页
        /*
         * pageNum :1 form 0 ,size 5 [0,1,2,3,4]
         * pageNum :2 form 5 ,size 5 [0,1,2,3,4]
         * form = (pageNum - 1) * size
         */
        Integer pageNum = param.getPageNum();
        int from = (pageNum - 1) * EsConstant.PRODUCT_PAGE_SIZE;
        searchSourceBuilder.from(from);
        searchSourceBuilder.size(EsConstant.PRODUCT_PAGE_SIZE);


        // 4.构建高亮
        if(!StringUtils.isEmpty(keyword)){
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            // 指定高亮字段
            highlightBuilder.field(EsConstant.SKU_TITLE);
            // 指定高亮的前置和后置标签
            highlightBuilder.preTags("<b style='color:red'>");
            highlightBuilder.postTags("</b>");
            searchSourceBuilder.highlighter(highlightBuilder);
        }


        // 4.构建聚合
        // 构建品牌聚合
        TermsAggregationBuilder BRAND_AGG = AggregationBuilders.terms(EsConstant.BRAND_AGG);
        BRAND_AGG.field(EsConstant.BRAND_ID).size(EsConstant.BRAND_AGG_SIZE);
        // 品牌名称子聚合
        TermsAggregationBuilder BRAND_NAME_AGG =
                AggregationBuilders.terms(EsConstant.BRAND_NAME_AGG).field(EsConstant.BRAND_NAME_AGG_FIELD).size(EsConstant.BRAND_NAME_AGG_SIZE);
        BRAND_AGG.subAggregation(BRAND_NAME_AGG);

        // 品牌图片子聚合
        TermsAggregationBuilder BRAND_IMG_AGG =
                AggregationBuilders.terms(EsConstant.BRAND_IMG_AGG).field(EsConstant.BRAND_IMG_AGG_FIELD).size(EsConstant.BRAND_IMG_AGG_SIZE);
        BRAND_AGG.subAggregation(BRAND_IMG_AGG);
        searchSourceBuilder.aggregation(BRAND_AGG);

        // 构建分类集合
        TermsAggregationBuilder CATALOG_AGG = AggregationBuilders.terms(EsConstant.CATALOG_AGG);
        CATALOG_AGG.field(EsConstant.CATALOG_ID).size(EsConstant.CATALOG_AGG_SIZE);

        // 分类的子聚合
        TermsAggregationBuilder CATALOG_NAME_AGG =
                AggregationBuilders.terms(EsConstant.CATALOG_NAME_AGG).field(EsConstant.CATALOG_NAME_AGG_FIELD).size(EsConstant.CATALOG_NAME_AGG_SIZE);
        CATALOG_AGG.subAggregation(CATALOG_NAME_AGG);
        searchSourceBuilder.aggregation(CATALOG_AGG);

        // 属性的聚合,使用nested聚合
        NestedAggregationBuilder ATTR_AGG = AggregationBuilders.nested(EsConstant.ATTR_AGG, EsConstant.NESTED_ATTR_PATH);
        // 属性的id子聚合
        TermsAggregationBuilder ATTR_ID_AGG =
                AggregationBuilders.terms(EsConstant.ATTR_ID_AGG).field(EsConstant.NESTED_ATTR_ID);
        // 属性id的子聚合的子聚合(属性名称)
        TermsAggregationBuilder ATTR_NAME_AGG
                = AggregationBuilders.terms(EsConstant.ATTR_NAME_AGG).field(EsConstant.NESTED_ATTR_NAME).size(EsConstant.ATTR_NAME_AGG_SIZE);
        // 属性id的子聚合的子聚合(属性值)
        TermsAggregationBuilder ATTR_VALUE_AGG
                = AggregationBuilders.terms(EsConstant.ATTR_VALUE_AGG).field(EsConstant.NESTED_ATTR_VALUES).size(EsConstant.ATTR_VALUE_AGG_SIZE);

        ATTR_ID_AGG.subAggregation(ATTR_NAME_AGG);
        ATTR_ID_AGG.subAggregation(ATTR_VALUE_AGG);

        ATTR_AGG.subAggregation(ATTR_ID_AGG);

        searchSourceBuilder.aggregation(ATTR_AGG);

        log.info("es检索构建的DSL语句----->>>{}",searchSourceBuilder.toString());

        // 5.指定es中的索引,以及SearchSourceBuilder
//        SearchRequest request = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},searchSourceBuilder);
        SearchRequest request = new SearchRequest();
        request.indices(EsConstant.PRODUCT_INDEX);
        request.source(searchSourceBuilder);
        return request;
    }

    /*
     * @param response
     * @param param
     * @==============History===============<br/>
     * @Description //   根据es返回数据,封装响应结果
     * @Date 2023/4/15 2023/4/15
     * @Author < a href="2277411582@qq.com">chen</ a>
     * @return
     */
    private SearchResult buildSearchResult(SearchResponse response, SearchParam param){
        SearchResult result = new SearchResult();

        // 1.返回所有查询到的商品
        List<SkuEsModel> products = new ArrayList<>();
        SearchHits hits = response.getHits();
        if(hits.getHits() != null && hits.getHits().length > 0){
            // 遍历命中的所有商品数据
            for (SearchHit hit : hits.getHits()) {
                String sourceAsString = hit.getSourceAsString();
                ObjectMapper mapper = new ObjectMapper();
                JavaType javaType = mapper.getTypeFactory().constructType(SkuEsModel.class);
                SkuEsModel esModel = null;
                try {
                    esModel = (SkuEsModel)mapper.readValue(sourceAsString, javaType);
                } catch (IOException e) {
                    e.printStackTrace();
                    log.info("转换es响应模型失败------>>>>{}",e.getMessage());
                }
                // 如果查询条件最终包含关键字查询,需要商品的标题高亮显示
                if(!StringUtils.isEmpty(param.getKeyword())){
                    HighlightField highlightField = hit.getHighlightFields().get(EsConstant.SKU_TITLE);
                    String skuTitleHighlight = highlightField.getFragments()[0].toString();
                    esModel.setSkuTitle(skuTitleHighlight);
                }

                products.add(esModel);
            }
        }
        result.setProducts(products);

//     ==============================以上从响应结果中的命中记录中获取=========================================

        // 2.返回所有商品的所有属性信息
        List<AttrVo> attrVos = new ArrayList<>();
        // 获取属性聚合信息
        ParsedNested ATTR_AGG = response.getAggregations().get(EsConstant.ATTR_AGG);
        ParsedLongTerms ATTR_ID_AGG = ATTR_AGG.getAggregations().get(EsConstant.ATTR_ID_AGG);
        // 获取属性id聚合的数据
        for (Terms.Bucket bucket : ATTR_ID_AGG.getBuckets()) {
            AttrVo attrVo = new AttrVo();
            // 获取到的属性id
            long attrId = bucket.getKeyAsNumber().longValue();
            // 根据这个属性id,获取属性id的子集合中的名称和值
            ParsedStringTerms ATTR_NAME_AGG = bucket.getAggregations().get(EsConstant.ATTR_NAME_AGG);
            String attrName = ATTR_NAME_AGG.getBuckets().get(0).getKeyAsString();

            ParsedStringTerms ATTR_VALUE_AGG = bucket.getAggregations().get(EsConstant.ATTR_VALUE_AGG);
            List<String> attrValues =
                    ATTR_VALUE_AGG.getBuckets()
                                  .stream()
                                  .map(item->item.getKeyAsString())
                                  .collect(Collectors.toList());
            // 设置属性信息
            attrVo.setAttrId(attrId);
            attrVo.setAttrName(attrName);
            attrVo.setAttrValue(attrValues);

            attrVos.add(attrVo);
        }

        result.setAttrs(attrVos);

        // 3.当前所有商品设计到的所有品牌信息
        List<BrandVo> brandVos = new ArrayList<>();
        // 获取品牌的聚合
        ParsedLongTerms BRAND_AGG = response.getAggregations().get(EsConstant.BRAND_AGG);
        for (Terms.Bucket bucket : BRAND_AGG.getBuckets()) {
            BrandVo brandVo = new BrandVo();
            // 获取到品牌的id
            long brandId = bucket.getKeyAsNumber().longValue();
            // 获取当前商品id聚合的名称子聚合和图片子聚合
            ParsedStringTerms BRAND_NAME_AGG = bucket.getAggregations().get(EsConstant.BRAND_NAME_AGG);
            String brandName = BRAND_NAME_AGG.getBuckets().get(0).getKeyAsString();

            ParsedStringTerms BRAND_IMG_AGG = bucket.getAggregations().get(EsConstant.BRAND_IMG_AGG);
            String brandImg = BRAND_IMG_AGG.getBuckets().get(0).getKeyAsString();

            brandVo.setBrandId(brandId);
            brandVo.setBrandName(brandName);
            brandVo.setBrandImg(brandImg);

            brandVos.add(brandVo);
        }


        result.setBrands(brandVos);



        // 4.当前所有商品设计的分类信息
        // 获取分类聚合
        List<CateLogVo> cateLogVos = new ArrayList<>();
        ParsedLongTerms CATALOG_AGG = response.getAggregations().get(EsConstant.CATALOG_AGG);
        for (Terms.Bucket bucket : CATALOG_AGG.getBuckets()) {
            CateLogVo cateLogVo = new CateLogVo();
            // 获取分类的id
            long cateLogId = bucket.getKeyAsNumber().longValue();

            // 获取分类的名称
            ParsedStringTerms CATALOG_NAME_AGG = bucket.getAggregations().get(EsConstant.CATALOG_NAME_AGG);
            String cateLogName = CATALOG_NAME_AGG.getBuckets().get(0).getKeyAsString();

            cateLogVo.setCateLogId(cateLogId);
            cateLogVo.setCateLogName(cateLogName);

            cateLogVos.add(cateLogVo);
        }

        result.setCatalogs(cateLogVos);

//     ==============================以上从响应结果中的聚合信息中获取=========================================

        // 5.分页信息
        // 页码
        result.setPageNum(param.getPageNum());
        // 总记录数
        long total = hits.getTotalHits().value;
        result.setTotal(total);
        // 总页数  total%PRODUCT_PAGE_SIZE = 0  total/PRODUCT_PAGE_SIZE
        int totalPage = total % EsConstant.PRODUCT_PAGE_SIZE == 0 ? (int)total / EsConstant.PRODUCT_PAGE_SIZE : ((int)total / EsConstant.PRODUCT_PAGE_SIZE + 1);
        result.setTotalPages(totalPage);

        // 设置分页导航条
        List<Integer> pageNavs = new ArrayList<>();
        for (int i = 1; i <= totalPage; i++) {
            pageNavs.add(i);
        }
        result.setPageNavs(pageNavs);


        // 6.面包屑导航功能
        // 根据查询条件中是否带了属性的查询条件
        List<String> attrs = param.getAttrs();
        if(attrs != null && attrs.size() > 0){
            List<SearchResult.NavVo> navVos = attrs.stream().map(attr -> {
                // attr保存的格式是1_1.5存:2.5存  _前是属性的id,_后是属性带的值
                // 分析每个传过来的参数值
                SearchResult.NavVo navVo = new SearchResult.NavVo();
                String[] str = attr.split("_");
                // 属性id
                String attrId = str[0];
                // 属性值
                String attrValues = str[1];
                navVo.setNavValue(attrValues);

                // 远程查询商品服务获取属性信息
                R r = productFeignService.attrInfo(attrId);
                if(r.getCode() == 0){
                    AttrResponseVo attrResponseVo = r.getData("data", new TypeReference<AttrResponseVo>() {});
                    navVo.setNavName(attrResponseVo.getAttrName());
                }else {
                    // 远程查询失败,使用id作为name
                    navVo.setNavName(attrId);
                }

                // 点击取消了这个面包屑,设置要跳转的路径,将请求路径中当前的这个属性置空
                String replaceQueryUrl = replaceQueryString(param.get_queryString(), attr, EsConstant.NESTED_ATTR_PATH);
                navVo.setLink(EsConstant.URL+replaceQueryUrl);
                log.info("面包屑拼接后的url访问地址------>>>{}",EsConstant.URL+replaceQueryUrl);
                // 属性id集合
                result.getAttrIds().add(Long.valueOf(attrId));
                return navVo;
            }).collect(Collectors.toList());
            result.setNavs(navVos);
        }


        // 品牌也需要上面包屑
        List<Long> brandId = param.getBrandId();
        log.info("检索条件中的品牌id------>>>>{}",brandId);
        if(brandId != null && brandId.size() > 0){
            // 先获取之前的面包屑导航,这里的NavVo中的link地址值替换自己的,这个就是把现在url请求的地址中包含自己的那个查询参数替换掉
            List<SearchResult.NavVo> navs = result.getNavs();
            SearchResult.NavVo navVo = new SearchResult.NavVo();
            navVo.setNavName(EsConstant.BRAND_NAME);
            R r = productFeignService.brandInfos(brandId);
            if(r.getCode() == 0){
                // 远程获取到所有的品牌信息
                List<BrandVo> brandVoList = r.getData("brand", new TypeReference<List<BrandVo>>() {});
                StringBuffer buffer = new StringBuffer();
                String replace = "";
                for (BrandVo brandVo : brandVoList) {
                    // 拼接品牌的名称作为values值在面包屑上展示
                    buffer.append(brandVo.getBrandName());
                    // 将所有的品牌的信息替换掉
                    replace = replaceQueryString(param.get_queryString(), brandVo.getBrandId().toString(), EsConstant.BRAND_ID);
                }
                navVo.setNavValue(buffer.toString());
                navVo.setLink(EsConstant.URL+replace);
            }
            navs.add(navVo);
        }


        return result;
    }

    /*
     * @param queryString  需要清空的目标url
     * @param value        需要清空的值
     * @param key          需要清空的属性名称
     * @==============History===============<br/>
     * @Description //   清空当前url地址中的值
     * @Date 2023/4/16 2023/4/16
     * @Author < a href="2277411582@qq.com">chen</ a>
     * @return [java.lang.String, java.lang.String, java.lang.String]
     */
    protected String replaceQueryString(String queryString,String value,String key){
        String encode = "";
        try {
            // 将需要清空的值按照url进行编码
            encode = URLEncoder.encode(value, "UTF-8");
            // 这是为了适配浏览器和java对url中的空格进行编码不一致问题处理  java会将空格转换为+,浏览器会将空格转换为%20
            encode.replace("+","%20");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        String replace = queryString.replace("&" + key + "=" + value, "").replace(key + "=" + value + "&", "");
        log.info("es面包屑解码后拼接的url查询条件----->>>{}",replace);
        return replace;
    }
}
